{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Read-Only and Computed Properties"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although write-only properties are not that common, read-only properties (i.e. that define a getter but not a setter) are quite common for a number of things."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, we can create read-only properties, but since nothing is private, at best we are \"suggesting\" to the users of our class they should treat the property as read-only. There's always a way to hack around that of course.\n",
    "\n",
    "But still, it's good to be able to at least explicitly indicate to a user that a property is meant to be read-only."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The use case I'm going to focus on in this video, is one of computed properties. Those are properties that may not actually have a backing variable, but are instead calculated on the fly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Consider this simple example of a `Circle` class where we can read/write the radius of the circle, but want a computed property for the area. We don't need to store the area value, we can alway calculate it given the current radius value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import pi\n",
    "\n",
    "class Circle:\n",
    "    def __init__(self, radius):\n",
    "        self.radius = radius\n",
    "        \n",
    "    @property\n",
    "    def area(self):\n",
    "        print('calculating area...')\n",
    "        return pi * (self.radius ** 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calculating area...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3.141592653589793"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c = Circle(1)\n",
    "c.area"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could certainly just use a class method `area()`, but the area is more a property of the circle, so it makes more sense to just retrive it as a property, without the extra `()` to make the call."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The advantage of how we did this is that shoudl the radius of the circle ever change, the area property will immediately reflect that."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calculating area...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "12.566370614359172"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.radius = 2\n",
    "c.area"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On the other hand, it's also a weakness - every time we need the area of the circle, it gets recalculated, even if the radius has not changed!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calculating area...\n",
      "calculating area...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "12.566370614359172"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.area\n",
    "c.area"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now we can use properties to fix this problem without breaking our interface!\n",
    "\n",
    "We are going to cache the area value, and only-recalculate it if the radius has changed.\n",
    "\n",
    "In order for us to know if the radius has changed, we are going to make it into a property, and the setter will keep track of whether the radius is set, in which case it will invalidate the cached area value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Circle:\n",
    "    def __init__(self, radius):\n",
    "        self.radius = radius\n",
    "        self._area = None\n",
    "        \n",
    "    @property\n",
    "    def radius(self):\n",
    "        return self._radius\n",
    "    \n",
    "    @radius.setter\n",
    "    def radius(self, value):\n",
    "        # if radius value is set we invalidate our cached _area value\n",
    "        # we could make this more intelligent and see if the radius has actually changed\n",
    "        # but keeping it simple\n",
    "        self._area = None\n",
    "        # we could even add validation here, like value has to be numeric, non-negative, etc\n",
    "        self._radius = value\n",
    "        \n",
    "    @property\n",
    "    def area(self):\n",
    "        if self._area is None:\n",
    "            # value not cached - calculate it\n",
    "            print('Calculating area...')\n",
    "            self._area = pi * (self.radius ** 2)\n",
    "        return self._area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = Circle(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Calculating area...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3.141592653589793"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3.141592653589793"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "c.radius = 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Calculating area...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "12.566370614359172"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "12.566370614359172"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c.area"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are a lot of other uses for calculate properties.\n",
    "\n",
    "Some properties may even do a lot work, like retrieving data from a database, making a call to some external API, and so on."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's write a class that takes a URL, downloads the web page for that URL and provides us some metrics on that URL - like how long it took to download, the size (in bytes) of the page."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although I am going to use the `urllib` module for this, I strongly recommend you use the `requests` 3rd party library instead: http://docs.python-requests.org"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib\n",
    "from time import perf_counter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class WebPage:\n",
    "    def __init__(self, url):\n",
    "        self.url = url\n",
    "        self._page = None\n",
    "        self._load_time_secs = None\n",
    "        self._page_size = None\n",
    "        \n",
    "    @property\n",
    "    def url(self):\n",
    "        return self._url\n",
    "    \n",
    "    @url.setter\n",
    "    def url(self, value):\n",
    "        self._url = value\n",
    "        self._page = None\n",
    "        # we'll lazy load the page - i.e. we wait until some property is requested\n",
    "        \n",
    "    @property\n",
    "    def page(self):\n",
    "        if self._page is None:\n",
    "            self.download_page()\n",
    "        return self._page\n",
    "    \n",
    "    @property\n",
    "    def page_size(self):\n",
    "        if self._page is None:\n",
    "            # need to first download the page\n",
    "            self.download_page()\n",
    "        return self._page_size\n",
    "        \n",
    "    @property\n",
    "    def time_elapsed(self):\n",
    "        if self._page is None:\n",
    "            self.download_page()\n",
    "        return self._load_time_secs\n",
    "            \n",
    "    def download_page(self):\n",
    "        self._page_size = None\n",
    "        self._load_time_secs = None\n",
    "        start_time = perf_counter()\n",
    "        with urllib.request.urlopen(self.url) as f:\n",
    "            self._page = f.read()\n",
    "        end_time = perf_counter()\n",
    "        \n",
    "        self._page_size = len(self._page)\n",
    "        self._load_time_secs = end_time - start_time"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "https://www.google.com \tsize=11_489 \telapsed=0.20 secs\n",
      "https://www.python.org \tsize=49_132 \telapsed=0.18 secs\n",
      "https://www.yahoo.com \tsize=524_548 \telapsed=0.77 secs\n"
     ]
    }
   ],
   "source": [
    "urls = [\n",
    "    'https://www.google.com',\n",
    "    'https://www.python.org',\n",
    "    'https://www.yahoo.com'\n",
    "]\n",
    "\n",
    "for url in urls:\n",
    "    page = WebPage(url)\n",
    "    print(f'{url} \\tsize={format(page.page_size, \"_\")} \\telapsed={page.time_elapsed:.2f} secs')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
